""" PsyBlockGame behavior implementation """
import math
import random
from copy import deepcopy
from PyQt4.QtGui import *
from PyQt4.QtCore import QPointF, SIGNAL, QObject
from PsyBlockGame.GameTimer import GameTimer

FIGURE_TYPES = [
    "Square",
    "Circle",
    "Rhomb",
    "Triangle",
]

DEFAULT_COLORS = [
    QColor("Tomato"),
    QColor("Orange"),
    QColor("DarkBlue"),
    QColor("Green"),
]

WHITE_COLOR = QColor("White")
BLACK_COLOR = QColor("Black")

START_COUNTER_FONT = "Comic Sans MS"

def createFigure(figure, config):
    typeName = FIGURE_TYPES[figure.typeId]
    creator = globals()['create' + typeName]
    return creator(config.figureWidth, config.figureBorderWidth, figure.color, config.figureBackColor)

def validate(val):
    default = type(val)()
    for name in default.__dict__:
        if name not in val.__dict__:
            setattr(val, name, getattr(default, name))

    for name in list(val.__dict__):
        if name not in default.__dict__:
            delattr(val, name)

#
# Figure configuration
#
class FigureConfig:
    def __init__(self, type = 'Square', color = None, key = ''):
        self.typeId = type if isinstance(type, int) else FIGURE_TYPES.index(type)
        self.color = color if color else DEFAULT_COLORS[self.typeId]
        self.key = key

    def copy(self):
        return FigureConfig(self.typeId, self.color)

    def validate(self):
        validate(self)

#
# Game configuration
#
class GameConfig:
    def __init__(self):
        self.figures = list(map(FigureConfig, FIGURE_TYPES))
        self.rotationRadius     = 80   # distance from center to each figure
        self.rotationSpeed      = 70   # degrees per second
        self.eachFigureRotationSpeed = 0
        self.figureWidth        = 40    
        self.figureBorderWidth  = 5
        self.figureBackColor    = WHITE_COLOR

        self.startCounterColor  = BLACK_COLOR
        self.startCounterFontSize = 50

        #self.startGameTime      = 2
        self.gameTime           = 10 # minutes
        self.startRoundTime     = 2
        self.flyAwayTimeFrom    = 3
        self.flyAwayTimeTo      = 7
        self.flyAwaySpeed       = 100
        self.flyAwayTimeout     = 1.5

    def validate(self):
        validate(self)

        if len(self.figures) > 4:
            del self.figures[4:]
        for p in self.figures:
            p.validate()

    def copy(self):
        return deepcopy(self)

#
# Game behaviour implementation
#
STATE_IDLE          = 0
STATE_START_GAME    = 1
STATE_START_ROUND   = 2
STATE_ROTATION      = 3
STATE_FLY_AWAY      = 4

class Game(QObject):
    def __init__(self, view, config = None):
        super(Game, self).__init__()
        super(Game, self).__init__()

        self.config = config if config is not None else GameConfig()
        self.scene = QGraphicsScene()
        view.setScene(self.scene)

        self.figures = []
        self.timer = GameTimer()
        self.resetRotationOffsets()

        self.currentRoundTime = self.config.flyAwayTimeFrom
        self.isPreviewMode = False

        self.stop()

    def stop(self):
        self.state = STATE_IDLE
        self.scene.clear()
        self.figures = []
        self.flyAwayFigure = None

    def prepareLevel(self, showFigures, previewMode):
        self.resetRotationOffsets()

        self.isPreviewMode = previewMode

        # create figures
        self.figures = [(createFigure(p, self.config), index) for index, p in enumerate(self.config.figures)]

        random.shuffle(self.figures)
        self.figureIndex = [index for _, index in self.figures]
        self.figures     = [p for p, _ in self.figures]

        self.flyAwayFigure = None

        m = QMatrix()
        baseAngle = 360 / len(self.figures)
        for index, p in enumerate(self.figures):
            m.reset()
            m.rotate(index * baseAngle)
            x, y = m.map(0, -self.config.rotationRadius)
            p.setPos(x, y)
            p.setVisible(showFigures)
            self.scene.addItem(p)

    def startPreviewSingle(self):
        self.stop()
        self.figures = [createFigure(self.config.figures[0], self.config)]
        self.scene.addItem(self.figures[0])
        self.isPreviewMode = True
        self.state = STATE_IDLE

    def startPreviewMotion(self):
        self.stop()
        self.prepareLevel(True, True)
        self.timer.start()
        self.state = STATE_ROTATION

    def startLevel(self):
        self.stop()
        self.calcRoundTime()
        self.prepareLevel(False, False)

        # start round except first counter
        self.startCounter = 3

        self.initStartText()
        self.timer.start(2 / 3) #self.config.startGameTime / 3)
        self.state = STATE_START_GAME

    def startRound(self):
        self.stop()
        self.calcRoundTime()
        self.prepareLevel(True, False)

        self.startCounter = 3
        
        self.initStartRoundDot()
        self.timer.start()
        self.state = STATE_START_ROUND

    def calcRoundTime(self):
        # calc random value in range: (1, roundTime]
        self.currentRoundTime = self.config.flyAwayTimeFrom + random.random() * (
            self.config.flyAwayTimeTo - self.config.flyAwayTimeFrom)

    def initStartText(self):
        self.startText = QGraphicsTextItem()
        self.scene.addItem(self.startText)

        self.startText.setDefaultTextColor(self.config.startCounterColor)
        self.startText.setFont(QFont(START_COUNTER_FONT, self.config.startCounterFontSize))
        m = QFontMetricsF(self.startText.font())

        text = str(self.startCounter)
        self.startText.setPlainText(text)

        self.startText.setPos(-m.width(text) / 2, -m.height() / 2)
        self.startText.setVisible(True)

    def initStartRoundDot(self):
        self.startRoundDot = QGraphicsEllipseItem(-4, -4, 8, 8)
        self.startRoundDot.setPos(QPointF(0, 0))
        self.startRoundDot.setBrush(QBrush(QColor("Black")))
        self.startRoundDot.setVisible(False)
        self.scene.addItem(self.startRoundDot)

    def update(self):
        if self.state == STATE_IDLE:
            return

        if self.state == STATE_START_GAME:
            if self.timer.expired():
                self.startCounter -= 1
                self.startText.setPlainText(str(self.startCounter))
                self.timer.restart()

                if self.startCounter == 0:
                    self.startText.setVisible(False)
                    for p in self.figures:
                        p.setVisible(True)

                    self.timer.start()
                    #self.state = STATE_ROTATION
                    #self.emit(SIGNAL("onRoundStarted()"))
                    self.startRound()

        if self.state == STATE_START_ROUND:
            timeout = self.config.startRoundTime / 3
            if self.startRoundDot.isVisible():
                timeout *= 2 / 5
            else:
                timeout *= 3 / 5
                
            if self.timer.expired(timeout, restart = True):
                self.startRoundDot.setVisible(not self.startRoundDot.isVisible())

                if not self.startRoundDot.isVisible():
                    self.startCounter -= 1

                if self.startCounter == 0:
                    self.startRoundDot.setVisible(False)
                    self.state = STATE_ROTATION
                    self.timer.start()
                    self.emit(SIGNAL("onRoundStarted()"))

        if self.state == STATE_ROTATION:
            self.updatePos()
            
            if not self.isPreviewMode:
                # start fly away
                if self.timer.expired(self.currentRoundTime):
                    index = random.randrange(len(self.figures))
                    self.flyAwayFigure = self.figures[index]
                    self.figures[index] = None
                    self.flyAwayFigurePos = self.flyAwayFigure.scenePos()
                    self.flyAwayFigureDir = self.calcFigureDirection(self.flyAwayFigure)

                    self.emit(SIGNAL("onFlyAwayStarted(int)"), self.figureIndex[index])
                    self.restartTimer()
                    self.state = STATE_FLY_AWAY

        if self.state == STATE_FLY_AWAY:
            self.updatePos()

            if self.timer.expired(self.config.flyAwayTimeout):
                # check for fly away timeout
                self.state = STATE_IDLE
                self.emit(SIGNAL("onFlyAwayTimeout()"))

    def calcFigureDirection(self, figure):
        if self.config.rotationSpeed > 10:
            rotationSign = 1
        elif self.config.rotationSpeed < -10:
            rotationSign = -1
        else:
            rotationSign = 0

        pos1 = figure.scenePos()
        m = QMatrix()

        if rotationSign != 0:
            m.rotate(15 * rotationSign)
            pos2 = m.map(pos1)
            dir = calcDir(pos1, pos2)
        else:
            dir = normalize(pos1)

        m.reset()
        m.rotate(rotationSign * -45 * (0.3 + random.random() * 0.7))
        dir = m.map(dir)

        return dir

    def resetRotationOffsets(self):
        self.rotationOffset = 0
        self.figureRotationOffset = 0

    def restartTimer(self):
        self.rotationOffset += self.timer.time() * self.config.rotationSpeed
        self.figureRotationOffset += self.timer.time() * self.config.eachFigureRotationSpeed
        self.timer.start()

    def updateStartRoundPos(self):
        m = QMatrix()
        baseAngle = 360 / len(self.figures)

        distance = self.config.rotationRadius\
        #min(self.config.rotationRadius,
        #    self.config.rotationRadius * 2 * self.timer.time() / self.config.startRoundTime)

        for index, p in enumerate(self.figures):
            if p is None:
                continue

            m.reset()
            m.rotate(self.rotationOffset + index * baseAngle)
            x, y = m.map(0, -distance)

            m.reset()
            p.setTransform(QTransform(m))
            p.rotate(self.figureRotationOffset + p.baseRotation)
            p.setPos(x, y)
            
    def updatePos(self):
        m = QMatrix()
        baseAngle = 360 / len(self.figures)
        time = self.timer.time()

        for index, p in enumerate(self.figures):
            if p is None:
                continue

            m.reset()
            m.rotate(self.rotationOffset + index * baseAngle + time * self.config.rotationSpeed)
            x, y = m.map(0, -self.config.rotationRadius)

            m.reset()
            p.setTransform(QTransform(m))
            p.rotate(self.figureRotationOffset + time * self.config.eachFigureRotationSpeed + p.baseRotation)
            p.setPos(x, y)

        if self.flyAwayFigure is not None:
            pos = move(self.flyAwayFigurePos, self.flyAwayFigureDir, self.timer.time() * self.config.flyAwaySpeed)
            self.flyAwayFigure.setPos(pos)

#
# GraphicsItem creation functions
#
def normalize(p):
    x = p.x()
    y = p.y()
    len = math.sqrt(x * x + y * y)
    p.setX(x / len)
    p.setY(y / len)
    return p

def calcDir(pFrom, pTo):
    dx = pTo.x() - pFrom.x()
    dy = pTo.y() - pFrom.y()
    return normalize(QPointF(dx, dy))

def move(start, dir, distance):
    x, y = start.x(), start.y()
    dx, dy = dir.x(), dir.y()
    return QPointF(x + dx * distance, y + dy * distance)

def _createRectItem(itemType, width, color, parent = None):
    item = itemType(-width / 2, -width / 2, width, width, parent)
    item.setPen(QPen(color))
    item.setBrush(QBrush(color))
    return item

def _createTriangleItem(width, color, parent = None):
    width_2 = width / 2
    height_2 = width_2 * math.tan(math.pi / 6)
    height_1 = width * math.sin(math.pi / 3) - height_2

    polygon = QPolygonF()
    polygon.append(QPointF(width_2, height_2))
    polygon.append(QPointF(-width_2, height_2))
    polygon.append(QPointF(0, -height_1))

    item = QGraphicsPolygonItem(polygon, parent)
    item.setPen(QPen(color))
    item.setBrush(QBrush(color))
    return item

def createSquare(width, border, color, backColor):
    width *= 0.95
    item = _createRectItem(QGraphicsRectItem, width, color)
    if width > border * 2:
        _createRectItem(QGraphicsRectItem, width - border * 2, backColor, item)

    item.baseRotation = 0
    return item

def createCircle(width, border, color, backColor):
    item = _createRectItem(QGraphicsEllipseItem, width, color)
    if width > border * 2:
        _createRectItem(QGraphicsEllipseItem, width - border * 2, backColor, item)

    item.baseRotation = 0
    return item

def createRhomb(width, border, color, backColor):
    # TODO: create correct rhomb
    width /= 1.1 # rotation correction
    item = _createRectItem(QGraphicsRectItem, width, color)
    item.rotate(45)
    if width > border * 2:
        _createRectItem(QGraphicsRectItem, width - border * 2, backColor, item)

    item.baseRotation = 45
    return item

def createTriangle(width, border, color, backColor):
    width *= 1.1
    item = _createTriangleItem(width, color)
    border /= 0.70 # math.cos(math.pi / 6)
    if width > border * 2:
        _createTriangleItem(width - border * 2, backColor, item)

    item.baseRotation = 0
    return item
